/**
 * Captain
 * © 2017 Captain copyright，All rights reserved.
 * http://www.sccaptain.com.cn
 * 
 * JAVA : 8
 * 文  件  名: RequestBodyJackson2SortArgumentResolver.java
 * 创  建  人: Roc Zheng (roc.djz@gmail.com)
 * 创建时间: 2017年7月6日 上午10:11:56
 * 版         本: 1.0.0
 * 备         注:
 * 修订历史:
 */
package com.material.c2c.web.bind;

import java.util.ArrayList;
import java.util.List;

import org.springframework.core.MethodParameter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.web.SortArgumentResolver;
import org.springframework.data.web.SortDefault;
import org.springframework.data.web.SortDefault.SortDefaults;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.cpt.framework.web.bind.JsonRequestBody;
import com.cpt.framework.web.bind.RequestBodyJackson2ArgumentResolver;
import com.fasterxml.jackson.databind.JsonNode;
import com.material.c2c.web.util.SpringDataAnnotationUtils;

/**
 * 
 * 
 * @since 1.0
 * @version 1.0
 * @author Roc Zheng (roc.djz@gmail.com)
 */
public class RequestBodyJackson2SortArgumentResolver extends RequestBodyJackson2ArgumentResolver implements SortArgumentResolver {

  private static final String DEFAULT_PARAMETER = "sort";
//  private static final String DEFAULT_PROPERTY_DELIMITER = ",";
//  private static final String DEFAULT_QUALIFIER_DELIMITER = "_";

  private static final String SORT_DEFAULTS_NAME = SortDefaults.class.getSimpleName();
  private static final String SORT_DEFAULT_NAME = SortDefault.class.getSimpleName();

  private Sort fallbackSort;
  private String sortParameter = DEFAULT_PARAMETER;
//  private String propertyDelimiter = DEFAULT_PROPERTY_DELIMITER;
//  private String qualifierDelimiter = DEFAULT_QUALIFIER_DELIMITER;

  /**
   * Configure the request parameter to lookup sort information from. Defaults to {@code sort}.
   * 
   * @param sortParameter must not be {@literal null} or empty.
   */
  public void setSortParameter(String sortParameter) {
    Assert.hasText(sortParameter, "sortParameter 不能为空。");
    this.sortParameter = sortParameter;
  }

//  /**
//   * Configures the delimiter used to separate property references and the direction to be sorted by. Defaults to
//   * {@code}, which means sort values look like this: {@code firstname,lastname,asc}.
//   * 
//   * @param propertyDelimiter must not be {@literal null} or empty.
//   */
//  public void setPropertyDelimiter(String propertyDelimiter) {
//
//    Assert.hasText(propertyDelimiter, "Property delimiter must not be null or empty!");
//    this.propertyDelimiter = propertyDelimiter;
//  }
//
//  /**
//   * Configures the delimiter used to separate the qualifier from the sort parameter. Defaults to {@code _}, so a
//   * qualified sort property would look like {@code qualifier_sort}.
//   * 
//   * @param qualifierDelimiter the qualifier delimiter to be used or {@literal null} to reset to the default.
//   */
//  public void setQualifierDelimiter(String qualifierDelimiter) {
//    this.qualifierDelimiter = qualifierDelimiter == null ? DEFAULT_QUALIFIER_DELIMITER : qualifierDelimiter;
//  }

  /* 
   * (non-Javadoc)
   * @see org.springframework.web.method.support.HandlerMethodArgumentResolver#supportsParameter(org.springframework.core.MethodParameter)
   */
  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return Sort.class.equals(parameter.getParameterType()) && super.supportsParameter(parameter);
  }

  @Override
  public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
    Sort defaultOrFallback = getDefaultFromAnnotationOrFallback(parameter);
    JsonNode json = readJsonRequestBody(parameter, webRequest, binderFactory);
    if (null == json) {
      return defaultOrFallback;
    }
    
    Sort sort = null;
    if (json.has(sortParameter)) {
      sort = parseJsonNode2Sort(json.get(sortParameter));
    }

    if (null == sort) {
      JsonNode targetJson = json.path(getJsonPath(parameter)).get(sortParameter);
      if (null != targetJson) {
        sort = parseJsonNode2Sort(targetJson);
      }
    }
    
    return null == sort ? defaultOrFallback : sort;
  }

  /**
   * Reads the default {@link Sort} to be used from the given {@link MethodParameter}. Rejects the parameter if both an
   * {@link SortDefaults} and {@link SortDefault} annotation is found as we cannot build a reliable {@link Sort}
   * instance then (property ordering).
   * 
   * @param parameter will never be {@literal null}.
   * @return the default {@link Sort} instance derived from the parameter annotations or the configured fallback-sort
   *         {@link #setFallbackSort(Sort)}.
   */
  private Sort getDefaultFromAnnotationOrFallback(MethodParameter parameter) {
    SortDefaults annotatedDefaults = parameter.getParameterAnnotation(SortDefaults.class);
    SortDefault annotatedDefault = parameter.getParameterAnnotation(SortDefault.class);
    if (annotatedDefault != null && annotatedDefaults != null) {
      throw new IllegalArgumentException(
          String.format("Cannot use both @%s and @%s on parameter %s! Move %s into %s to define sorting order!",
              SORT_DEFAULTS_NAME, SORT_DEFAULT_NAME, parameter.toString(), SORT_DEFAULT_NAME, SORT_DEFAULTS_NAME));
    }

    if (annotatedDefault != null) {
      return appendOrCreateSortTo(annotatedDefault, null);
    }

    if (annotatedDefaults != null) {
      Sort sort = null;
      for (SortDefault currentAnnotatedDefault : annotatedDefaults.value()) {
        sort = appendOrCreateSortTo(currentAnnotatedDefault, sort);
      }
      return sort;
    }

    return fallbackSort;
  }

  /**
   * Creates a new {@link Sort} instance from the given {@link SortDefault} or appends it to the given {@link Sort}
   * instance if it's not {@literal null}.
   * 
   * @param sortDefault
   * @param sortOrNull
   * @return
   */
  private Sort appendOrCreateSortTo(SortDefault sortDefault, Sort sortOrNull) {
    String[] fields = SpringDataAnnotationUtils.getSpecificPropertyOrDefaultFromValue(sortDefault, "sort");

    if (fields.length == 0) {
      return null;
    }

    Sort sort = new Sort(sortDefault.direction(), fields);
    return sortOrNull == null ? sort : sortOrNull.and(sort);
  }
  
  private String getJsonPath(MethodParameter parameter) {
    String path = parameter.getParameterName();
    if (parameter.hasParameterAnnotation(JsonRequestBody.class)) {
      JsonRequestBody jsonPathRequestBody = parameter.getParameterAnnotation(JsonRequestBody.class);
      if (StringUtils.hasText(jsonPathRequestBody.path())) {
        path = jsonPathRequestBody.path();
      }
    }
    return path;
  }
  
  private Sort parseJsonNode2Sort(JsonNode json) {
    if (json.isArray() && 0 < json.size()) {
      List<Order> orders = new ArrayList<>(json.size());
      for (JsonNode item : json) {
        Order order = parsonJsonNode2Order(item);
        if (null != order) {
          orders.add(order);
        }
      }
      
      return orders.isEmpty() ? null : new Sort(orders);
    }
    
    return null;
  }
  
  private Order parsonJsonNode2Order(JsonNode json) {
    JsonNode fieldNode = json.get("field");
    if (null == fieldNode) {
      return null;
    }
    
    String field = fieldNode.asText(null);
    if (null == field) {
      return null;
    }
    
    Direction direction = null;
    JsonNode orderNode = json.get("order");
    if (null != orderNode) {
      direction = Direction.fromStringOrNull(orderNode.asText("null"));
    }
    
    return new Order(direction, field);
  }
}
